package org.erikaredmark.monkeyshines.editor.model; import java.util.HashSet; import java.util.Set; import org.erikaredmark.monkeyshines.TileMap; import org.erikaredmark.monkeyshines.tiles.CommonTile; import org.erikaredmark.monkeyshines.tiles.TileType; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; /** * * Models the idea of a 'set' of tiles of any type, of any id, in some standard configuration. Templates can be created, loaded, and * saved by the editor and assist in designing levels that, for design purposes, tiles designed to go with each other to form one * bigger object (such as the tree leaves in 'In The Swing', or cobwebs in 'Spooked' * <p/> * Instances of this class are immutable. When building or adding to a template, the builder class is used to create a new template. * * @author Erika Redmark * */ public final class Template { /** * * Draws the given template to the level tiles. The passed array should be the same array * backing the LevelScreen that we wish to draw the template onto, hence it is a 20x32 array. * <p/> * Drawing by default starts at row, col as the <strong>top-left</strong> of the template. However, * the row/col offsets can be passed to change this, hence making the location be, for example, the * center of a 3x3 template by passing a rowOffset of 1 and a colOffset of 1. Negatives values are allowed * but probably wouldn't make much sense... the location clicked wouldn't be within the template range. * <p/> * The default state for drawing is that both empty and non-empty tiles that are to be replaced by this * template, will be replaced. Any empty spaces in the 2D 'rectangle' that this template takes up * are NOT part of the template. Only tiles specifically indicated as 'NO TILE' in the template will * replace their position with emptiness. * <p/> * If any template tiles would otherwise be drawn outside the map boundaries, they are simply not drawn; no * exceptions are thrown or assertions fired. * <p/> * All drawn tiles are copied, so state is not shared. * * @param map * tileMap for the level tiles in either the level screen being edited, or some other template * * @param row * the row of the 'start' of drawing * * @param col * the col of the 'start' of drawing * * @param rowOffset * row offset from the top-left of the template that this should start drawing (see docs above for example) * * @param colOffset * col offset from the top-left of the template that this should start drawing (see docs above for example) * */ public void drawTo(final TileMap map, int row, int col, int rowOffset, int colOffset) { // Example of offset /* * Template: * [X] [X] * [ ] [X] * * Click on row 1 col 1: * [ ] [ ] [ ] [ ] * [ ] [O] [X] [ ] * [ ] [ ] [X] [ ] * [ ] [ ] [ ] [ ] * * click on row 1 col 1 with +1 on both offsets * [X] [X] [ ] [ ] * [ ] [O] [ ] [ ] * [ ] [ ] [ ] [ ] * [ ] [ ] [ ] [ ] * */ final int topLeftRow = row - rowOffset; // if we move the centre inward, the top-left moves outward. final int topLeftCol = col - colOffset; // (topLeftRow, topLeftCol) is the zero point. In the template, if a tile is at 0,0, it is at THAT location in // the real world. If a tile is at 1, 0, it would be at (topLeftRow + 1, topLeftCol) and so on until it is drawn. // if a tile is out of bounds, it is silently skipped. for (final TemplateTile t : templateTiles) { int drawRow = topLeftRow + t.row; int drawCol = topLeftCol + t.col; if ( drawRow >= 0 && drawRow < 20 && drawCol >= 0 && drawCol < 32) { map.setTileRowCol(drawRow, drawCol, t.tile.copy() ); } } } private Template(final ImmutableList<TemplateTile> templateTiles) { this.templateTiles = templateTiles; } /** * * Creates a new tilemap that snuggly fits this template into a rectangular region. In the unlikely event this template * represents nothing, the returned tilemap is 1 tile by 1 tile and contains empty space * * @return * a new tilemap that is sized to perfectly fit this template * */ public TileMap fitToTilemap() { // We need two passes. First pass, get the maximum row/col range to build the map. Second: actually place the tiles // on the map int maxRow = 0; int maxCol = 0; for (TemplateTile tile : this.templateTiles) { if (tile.row > maxRow) maxRow = tile.row; if (tile.col > maxCol) maxCol = tile.col; } // Maximum row INDEX is not SIZE, hence the +1 TileMap fitMap = new TileMap(maxRow + 1, maxCol + 1); for (TemplateTile tile : this.templateTiles) { fitMap.setTileRowCol(tile.row, tile.col, tile.tile); } return fitMap; } /** * * Creates a template from a pre-existing tilemap. Note that any empty tiles are interpreted as * nothing. This means the resulting template will not place empty tiles in those positions; they will * just be considered not part of the template. * * @return * an instance of this object * */ public static Template fromTileMap(TileMap map) { Template.Builder builder = new Template.Builder(); TileType[] tiles = map.internalMap(); for (int i = 0; i < tiles.length; ++i) { if (tiles[i].equals(CommonTile.NONE) ) continue; int row = i / map.getColumnCount(); int col = i % map.getColumnCount(); builder.addTile(row, col, tiles[i]); } return builder.build(); } /** * * Returns a template builder class representing the state of this template so it can be mutated. Modifying the returned * builder does not affect this object. * * @return * new builder object with the initial state of this template * */ public Template.Builder mutableBuilder() { Template.Builder builder = new Template.Builder(); for (TemplateTile t : templateTiles) { builder.addTile(t.row, t.col, t.tile); } return builder; } /** * * Returns an immutable list of the tiles that make up this template, in case special processing is required * beyond what the API offer (typically for drawing code) * * @return * immutable list of tiles in this template * */ public ImmutableList<TemplateTile> getTilesInTemplate() { return templateTiles; } /** * * Two Templates are considered equal to each other if they map to the same tilemap when drawn. * */ @Override public boolean equals(Object o) { if (o == this) return true; if ( !(o instanceof Template) ) return false; Template other = (Template) o; // Ordering of tile in template list may be different even tiles are the same. What matters is that, when a tilemap is created, // they create an identical tilemap. TileMap myMap = this.fitToTilemap(); TileMap otherMap = other.fitToTilemap(); return myMap.equals(otherMap); } /** * * Two Templates are considered equal to each other if they map to the same tilemap when drawn. * */ @Override public int hashCode() { int result = 17; TileMap myMap = this.fitToTilemap(); result += result * 31 + myMap.hashCode(); return result; } @Override public String toString() { StringBuilder builder = new StringBuilder(); for (TemplateTile t : templateTiles) { builder.append(t.toString() ).append(System.lineSeparator() ); } return builder.toString(); } /** * * Allows one to construct instances of templates. The builder allows the addition, one at a time, of tiles to the template. This fits * in with the way a template would typically be constructed by a user (one at a time) * <p/> * Since the builder can be used with a GUI, it allows a callback to be assigned whenever the underlying template is modified. * * @author Erika Redmark * */ public static final class Builder { public Builder() { this(NO_FUNCTION); }; public Builder(final Function<Set<TemplateTile>, Void> callback ) { this.callback = callback; } /** * * Adds the given tile to the given position in this template. If a tile already exists at that position, it is bumped out and replaced. * <p/> * If this builder has a callback registered, that callback is called. * * @param row * row of tile, relative to the template design (not the world) * * @param col * column of tile, relative to the template design (not the world) * * @param tile * the tile type to create * * @return * this builder * */ public Builder addTile(int row, int col, TileType tile) { tiles.add(new TemplateTile(row, col, tile) ); callback.apply(tiles); return this; } /** * * Removes the tile at the given location. This is NOT the same as setting it to the empty tile (which means the template * would replace whatever was there with 'empty'). It means nothing will be at that location. * <p/> * Does nothing if there isn't anything there to begin with. * * @param row * row of tile, relative to the template design (not the world) * * @param col * column of tile, relative to the template design (not the world) * * @return * this builder * */ public Builder removeTile(int row, int col) { // Tile type is irrelevant; equals and hashcode don't care. tiles.remove(new TemplateTile(row, col, CommonTile.NONE) ); return this; } /** * * Creates a new instance of the enclosing Template class. Calls to subsequent builds always generate new objects. * * @return * new instance of the template based on the builder * */ public Template build() { return new Template(ImmutableList.copyOf(tiles) ); } // A set so that duplicates (tiles in the same position) are properly removed. Replaced with basic array list when converted // to a standard template since the only operation there is iteration over the list. private final Set<TemplateTile> tiles = new HashSet<TemplateTile>(); private final Function<Set<TemplateTile>, Void> callback; } // Intended for inner builder class, but Java rules require it to be declared outside. private static final Function<Set<TemplateTile>, Void> NO_FUNCTION = new Function<Set<TemplateTile>, Void>() { @Override public Void apply(Set<TemplateTile> arg0) { return null; } }; /** * * Represents a single 'tile' of the templates. Templates are composed of many of these in different configurations. These * represent a row/col in some imaginary space. * <p/> * Tiles cannot overlap. Hence, equality of this object is defined by the row and column ONLY, NOT the tile type! This allows * easy replacement * <p/> * * @author Erika Redmark * */ public static final class TemplateTile { public final int row; public final int col; public final TileType tile; public TemplateTile(int row, int col, final TileType tile) { this.row = row; this.col = col; this.tile = tile; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof TemplateTile) ) return false; TemplateTile object = (TemplateTile) o; return object.col == this.col && object.row == this.row; } @Override public int hashCode() { int result = 17; result += result * 31 + row; result += result * 31 + col; return result; } @Override public String toString() { return "Tile: " + tile + " at row [ " + row + " ] and col [ " + col + " ]"; } } // Stores list of all tiles. We don't store them in a 2D array. We just need to iterate over them, examine their row/col, and // from that decide where to draw the tile in the real world. A 2D array would be wasteful private ImmutableList<TemplateTile> templateTiles; }